# Created by 黄景涛
# DATE 2024/7/18


import logging
import json
import time
import allure
import pytest
from selenium.webdriver import ActionChains
from webdriver_helper import get_webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as EC

from base.login_util import login
from selenium.webdriver.common.keys import Keys

logger = logging.getLogger(__name__)
with open('./base/config.json', mode='r', encoding='utf8') as f:
    cfg = json.load(f)


def itf_login(username, password):
    """ 接口-登录 """
    login_url = cfg.get('login_info').get('login_url')
    return login(login_url, username, password)


def ui_login(driver):
    """ UI-登录 """
    login_msg = cfg.get('login_info')
    driver.get(login_msg['login_url'])
    kw = KeyWordLib(driver)
    kw.key_input(login_msg['user_input'], login_msg['username'])
    kw.key_input(login_msg['password_input'], login_msg['password'])
    kw.key_click(login_msg['login_button'])


class BaseDriver:
    @pytest.fixture(scope="class", autouse=True)
    def setup_class(self, request):
        # 打开Chrome浏览器
        self.driver = get_webdriver()
        self.wait = WebDriverWait(self.driver, 10)  # 显示等待最多10s
        self.driver.maximize_window()
        # 在测试类中共享setup_class方法
        request.cls.driver = self.driver
        # TODO 每执行一条用例前，自动登录
        ui_login(self.driver)
        yield
        # 测试完成后关闭浏览器
        self.driver.quit()


class KeyWordLib:
    def __init__(self, driver, timeout=10):
        self.driver: WebDriver = driver
        self.timeout = timeout

    # 内置，获取关键字函数
    def get_kw_method(self, key):
        f = getattr(self, f'key_{key}', None)
        if not f:
            raise AttributeError(f'不存在关键字:{key}')
        return f

    # 内置，等待元素出现
    def wait_for_element(self, by, locator):
        """
        :param self:
        :param by: 定位方式，eg. By.XPATH
        :param locator: 元素定位语句
        :return:
        """
        return WebDriverWait(self.driver, self.timeout).until(
            EC.element_to_be_clickable((by, locator)) or EC.visibility_of_element_located((by, locator))
        )

    # 内置，等待元素消失
    def wait_for_element_display(self, by, locator):
        return WebDriverWait(self.driver, self.timeout).until_not(
            EC.presence_of_element_located((by, locator))
        )

    # 内置，查找元素
    def find_element(self, loc: str):
        "封装过的元素自动使用显示等待"
        by_types = [By.CLASS_NAME, By.ID, By.NAME, By.LINK_TEXT, By.PARTIAL_LINK_TEXT]
        if loc.startswith(('.//', '//', '/')):
            by_type = By.XPATH
        elif loc.startswith(("#", ".")):
            by_type = By.CSS_SELECTOR
        else:
            for by_type in by_types:
                try:
                    el: WebElement = self.wait_for_element(by_type, loc)
                    tag_name = el.tag_name
                except:
                    continue
                else:
                    logger.info(f'正在定位元素{by_type, loc}')
                    logger.info(f'元素定位成功{el.tag_name}({el.rect})')
                    self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", el)  # 元素滚动到视图的正中间
                    return el
            else:
                by_type = By.XPATH
        try:
            el: WebElement = self.wait_for_element(by_type, loc)
            tag_name = el.tag_name
            logger.info(f'正在定位元素{by_type, loc}')
            logger.info(f'元素定位成功{el.tag_name}({el.rect})')
            self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", el)
            return el
        except Exception as e:
            loc_error = f'元素【{loc}】，所有定位方式都没有定位成功！'
            logger.error(loc_error)
            raise Exception(loc_error)

    # 动作：切换登录用户
    def key_switch_user(self, username, password, into_url=None):
        """
        :param self:
        :param username: 用户名
        :param password: 密码
        :param into_url: 需要进入的新页面
        :return:
        """
        # print(f'上一个用户的cookie: {self.driver.get_cookies()}')
        cookie = itf_login(username, password)
        self.driver.delete_all_cookies()
        for k, v in cookie.items():
            self.driver.add_cookie(dict(name=k, value=v))
        # print(f'下一个用户的cookie: {self.driver.get_cookies()}')
        self.driver.refresh()
        if into_url is not None:
            self.key_get(into_url)

    # 动作：地址跳转
    def key_get(self, url):
        self.driver.get(url)

    # 动作：为指定的元素执行js代码
    def key_jscode(self, loc, code):
        ele = self.find_element(loc)
        self.driver.execute_script(code, ele)

    # 动作：点击
    def key_click(self, loc):
        """
        :param self:
        :param loc: 元素定位
        :return:
        """
        ele = self.find_element(loc)
        ele.click()

    # 动作：输入
    def key_input(self, loc, content=None):
        """
        :param self:
        :param loc: 输入框定位
        :param content: 输入内容
        :return:
        """
        ele = self.find_element(loc)
        ele.clear()
        if content is not None:
            ele.send_keys(content)

    # 组件：单行文本字段
    def key_text_field(self, type_args, content=None):
        """
        :param self:
        :param type_args: by_name传form定位和字段名称，by_loc正常传如xpath
        :param content: 输入内容
        :return:
        """
        loc_type, *args = type_args.replace('，', ',').split(',')

        if loc_type.strip() == 'by_loc':
            text_field = self._location_field_by_loc(*args)
        elif loc_type.strip() == 'by_name':
            text_field = self._location_field_by_name(*args)
        else:
            raise Exception('loc_type参数有误')
        text_field.clear()
        if content is not None:
            text_field.send_keys(content)

    # 动作：输入并回车查询
    def key_input_and_enter(self, loc, content=None):
        """
        :param self:
        :param loc: 输入框定位
        :param content: 输入内容
        :return:
        """
        ele = self.find_element(loc)
        ele.clear()
        if content is not None:
            ele.send_keys(content + Keys.ENTER)

    # 动作：等待n秒
    def key_sleep(self, sec: float = 2):
        time.sleep(float(sec))

    # 动作：拖拽节点
    def key_drag_node(self, node_name, off_x, off_y):
        """
        :param self:
        :param node_name: 节点类型，中文名称
        :param off_x: 相对于画布的开始节点，x坐标，e.g: 100
        :param off_y: 相对于画布的开始节点，y坐标，e.g: 200
        :return:
        """
        # 各类卡片的定位信息
        node_data_types = {
            "START": "开始节点",
            "END": "结束节点",
            "NORMAL": "手动节点",
            "TASK": "API节点",
            "TASK-SOPS": "运维节点",
            "SIGN": "会签节点",
            "APPROVAL": "审批节点",
            "ROUTER-P": "并行网关",
            "COVERAGE": "汇聚网关",
        }

        # 生成XPATH路径（也可以用其他定位方式）
        for k, v in node_data_types.items():
            if node_name.strip() in v:
                xpath_loc = f'//*[@data-type="{k}"]'  # 仅ITSM-V3.x适用
                break
        else:
            raise Exception(f'未找到【{node_name}】类型的节点！')

        def drag_and_drop_to_offset(element, offset_x, offset_y):
            """ 拖拽到指定位置释放 """
            action = ActionChains(self.driver)
            # 拖放到指定位置
            action.click_and_hold(element).move_by_offset(offset_x, offset_y).release().perform()

        # 以【开始】节点作为参照定位
        start_loc = '.startpoint'  # ITSM-V3.x适用
        start_node = self.find_element(start_loc)
        # 获取开始节点的xy坐标
        start_x = start_node.location['x']
        start_y = start_node.location['y']

        # 定位节点卡片
        node_ele = self.find_element(xpath_loc)
        node_x = node_ele.location['x']
        node_y = node_ele.location['y']

        # 在画布中的位置计算
        target_x = start_x + off_x - node_x
        target_y = start_y + off_y - node_y
        # 拖拽节点到画布
        drag_and_drop_to_offset(node_ele, target_x, target_y)

    # 动作：线条连线
    def key_line_connect(self, source_, target_):
        """
        :param self:
        :param source_: 源节点名称和连接点，  e.g: 提单,右   --连接点分四个方向：上下左右
        :param target_: 目标节点名称和连接点，  e.g: 新增节点,左
        :return:
        """

        def up_down_left_right_by_relative(node):
            """ 计算上下左右连接点，相对节点的偏移坐标 """

            # 从WebElement实例，获取属性：节点卡片的宽、高
            width, height = node.rect['width'], node.rect['height']
            # 余量，目前连接点是个圆圈，尺寸16*16，半径8
            margin_size = 5
            up = (0, -(height // 2 + margin_size))
            down = (0, (height // 2 + margin_size))
            left = (-(width // 2 + margin_size), 0)
            right = ((width // 2 + margin_size), 0)
            sides_info = {"上": up, "下": down, "左": left, "右": right}
            return sides_info

        def node_ele(node_name):
            """ 从画布中，按照节点名称定位节点 """
            nonlocal self
            # 定位画布
            canvas = self.driver.find_element(By.ID, 'canvas-flow')  # 仅ITSM-3.x适用
            # 查找所有ID包含“node”的子级div
            nodes = canvas.find_elements(By.XPATH, './/div[contains(@id, "node")]')  # 仅ITSM-3.x适用
            for node in nodes:
                demo = node.text
                if node.text == node_name or node.text.startswith(node_name):
                    return node
            logger.error(f'未找到【{node_name}】节点')

        # excel中的参数：e.g, 提单,右
        source_node_name, source_node_side = source_.strip().replace('，', ',').split(',')
        target_node_name, target_node_side = target_.strip().replace('，', ',').split(',')

        # 源、目标节点对象
        source_node = node_ele(source_node_name)
        target_node = node_ele(target_node_name)

        # 节点的连接点，相对偏移坐标
        source_side_rel_xy = up_down_left_right_by_relative(source_node).get(source_node_side)
        target_side_rel_xy = up_down_left_right_by_relative(target_node).get(target_node_side)

        actions = ActionChains(self.driver)
        # 模拟从源节点偏移位，到目标节点偏移位的拖拽操作
        actions.move_to_element_with_offset(source_node, *source_side_rel_xy).click_and_hold()
        actions.move_to_element_with_offset(target_node, *target_side_rel_xy).release()
        actions.perform()

    # 动作：点击节点
    def key_click_node(self, node_name: str):
        """ 点击进入节点详情 """

        def node_ele(node_name):
            """ 从画布中，按照节点名称定位节点 """
            nonlocal self
            # 定位画布
            canvas = self.driver.find_element(By.ID, 'canvas-flow')  # 仅ITSM-3.x适用
            # 查找所有ID包含“node”的子级div
            nodes = canvas.find_elements(By.XPATH, './/div[contains(@id, "node")]')  # 仅ITSM-3.x适用
            for node in nodes:
                demo = node.text
                if node.text == node_name or node.text.startswith(node_name):
                    return node
            logger.error(f'未找到【{node_name}】节点')

        node = node_ele(node_name)
        node.click()

    # 动作：点击线条
    def key_click_line(self, line_name: str):
        """ 点击进入线条详情 """

        def node_ele(node_name):
            """ 从画布中，按照节点名称定位节点 """
            nonlocal self
            # 定位画布
            canvas = self.driver.find_element(By.ID, 'canvas-flow')  # 仅ITSM-3.x适用
            # 查找所有ID包含“node”的子级div
            nodes = canvas.find_elements(By.XPATH, './/div[contains(@id, "jsPlumb")]')  # 仅ITSM-3.x适用
            for node in nodes:
                demo = node.text
                span_node = node.find_element(By.TAG_NAME, "span")
                if span_node.text == node_name or span_node.text.startswith(node_name):
                    return span_node
            logger.error(f'未找到【{node_name}】线条')

        node = node_ele(line_name)
        node.click()

    # 内置，收起下拉框
    def _back_up_select(self, select_ele):
        """ 收起下拉 """
        # 获取WebElement实例的宽、高
        width, height = select_ele.rect['width'], select_ele.rect['height']
        offset_xy = (-(width//2), -(height//2))
        actions = ActionChains(self.driver)
        # 光标移动到下拉框的左上角，然后单击
        actions.move_to_element_with_offset(select_ele, *offset_xy).click().perform()

    # 内置，按字段名称定位
    def _location_field_by_name(self, form_loc: str, field_name: str):
        """
        :param self:
        :param form_loc: 字段所在的form表单
        :param field_name: 字段名称
        :return:
        """
        form = self.find_element(form_loc)
        ticket_fields = form.find_elements(By.XPATH, './/div[contains(@id, "bk-field")]')  # 适配1：工单字段
        common_fields = form.find_elements(By.XPATH, './/div[contains(@class, "bk-form-item")]')  # 适配2：普通页面的表单字段
        # 还可以添加其他适配...
        fields = common_fields or ticket_fields
        for field in fields:
            if field_name in field.text:
                return field
        raise Exception(f'不存在字段【{field_name}】')

    # 内置，按普通定位方式，定位字段
    def _location_field_by_loc(self, loc):
        """
        :param self:
        :param loc: 定位参数
        :return:
        """
        return self.find_element(loc)

    # 通用：单选下拉
    def key_select(self, type_args, option=None):
        """
        :param self:
        :param type_args: by_name传form定位和字段名称，by_loc传如xpath等
        :param option: 指定选项，两种写法：1、第n个选项，传入数字n；2、传入选项名称
        :return:
        """
        # 单选下拉对象
        loc_type, *args = type_args.replace('，', ',').split(',')

        if loc_type.strip() == 'by_loc':
            select = self._location_field_by_loc(*args)
        elif loc_type.strip() == 'by_name':
            select = self._location_field_by_name(*args)
        else:
            raise Exception('loc_type参数有误')
        # 点击展开下拉框
        select.click()

        try:
            # 选项集ul对象
            options_ul_loc = '//*[@class="tippy-popper"]/div/div[2]/div/div/div[2]/ul'  # 仅ITSM-3.x适用
            options_ul = self.find_element(options_ul_loc)
            try:
                # 若传选项索引
                option_idx = int(option)
                option_loc = options_ul_loc + f'/li[{option_idx}]'
                li = self.find_element(option_loc)
                li.click()
            except ValueError:
                # 获取ul的所有li，找出text包含的li，并选择第一个
                lis = options_ul.find_elements(By.TAG_NAME, 'li')
                li_span_loc = ".//div/div/span"
                for i, li in enumerate(lis):
                    try:
                        # 查找li中的<span>元素
                        span = li.find_element(By.XPATH, li_span_loc)
                        # 输出<span>元素的文本
                        # print(f'选项{i + 1}内容: {span.text}')
                        if option in span.text:
                            li.click()
                            logger.info(f"选项{i + 1}符合")
                            break
                    except Exception:
                        pass
                else:
                    error = "所有选项定位错误，或无匹配选项"
                    logger.error(error)
                    raise Exception(error)
        except:
            self._back_up_select(select)
            raise

    # 通用：多选下拉
    def key_multiselect(self, type_args, options: str = None):
        """
        :param self:
        :param type_args: by_name传form定位和字段名称，by_loc传如xpath等
        :param options: 多个下拉选项， e.g: 1,2   e.g: 选项1,选项3
        :return:
        """
        # 多选下拉对象
        loc_type, *args = type_args.replace('，', ',').split(',')
        if loc_type.strip() == 'by_loc':
            multiselect = self._location_field_by_loc(*args)
        elif loc_type.strip() == 'by_name':
            multiselect = self._location_field_by_name(*args)
        else:
            raise Exception('loc_type参数有误')
        # 点击展开下拉框
        multiselect.click()

        try:
            # 选项集ul对象
            options_ul_loc = '//*[@class="tippy-popper"]/div/div[2]/div/div/div[2]/ul'  # 仅ITSM-3.x适用
            options_ul = self.find_element(options_ul_loc)
            try:
                # 若传选项索引
                options_list = [int(idx) for idx in options.strip().replace('，', ',').split(',')]
                for idx in options_list:
                    option_loc = options_ul_loc + f'/li[{idx}]'
                    li = self.find_element(option_loc)
                    li.click()
            except ValueError:
                # 获取ul的所有li，找出text包含的li
                options_list = [name.strip() for name in options.strip().replace('，', ',').split(',')]
                lis = options_ul.find_elements(By.TAG_NAME, 'li')
                li_span_loc = ".//div/div/span"
                for option_name in options_list:
                    for i, li in enumerate(lis):
                        try:
                            # 查找li中的<span>元素
                            span = li.find_element(By.XPATH, li_span_loc)
                            if option_name in span.text:
                                li.click()
                                logger.info(f"选项{i + 1}符合")
                                break
                        except Exception as e:
                            pass
                    else:
                        not_find_msg = f"【{option_name}】选项未找到，跳过勾选"
                        logger.info(not_find_msg)
        except:
            raise
        finally:
            self._back_up_select(multiselect)

    # 通用组件：搜索并单选下拉      -- 只适用于DOM文档不剔除其他选取的组件，如优先级
    # TODO -- 待解决：sleep_sec问题
    def key_common_select_with_search(self, type_args, search_for: str = None, sleep_sec=1):
        """
        :param self:
        :param type_args: by_name传form定位和字段名称，by_loc传如xpath等
        :param search_for: 搜索的字符串内容
        :param sleep_sec: 搜索后等待
        :return:
        """
        # 单选下拉对象
        loc_type, *args = type_args.replace('，', ',').split(',')
        if loc_type.strip() == 'by_loc':
            select = self._location_field_by_loc(*args)
        elif loc_type.strip() == 'by_name':
            select = self._location_field_by_name(*args)
        else:
            raise Exception('loc_type参数有误')
        # 点击展开下拉框
        select.click()

        try:
            # 选项集ul对象
            options_ul_loc = '//*[@class="tippy-popper"]/div/div[2]/div/div/div[2]/ul'  # 仅ITSM-3.x适用
            options_ul = self.find_element(options_ul_loc)
            all_lis = options_ul.find_elements(By.TAG_NAME, 'li')

            # 内部搜索框对象
            select_search_loc = '//*[@class="tippy-popper"]/div/div[2]/div/div/div[1]/input'
            self.key_input(select_search_loc, search_for)
            self.key_sleep(sleep_sec)

            visible_lis = [li for li in all_lis if li.value_of_css_property('display') != 'none']
            if not len(visible_lis):
                null_error = "暂无选项"
                raise Exception(null_error)

            li_1 = visible_lis[0]
            if search_for in li_1.text:
                li_1.click()
            else:
                no_such_option_error = f"选项【{search_for}】搜索不出来"
                raise Exception(no_such_option_error)
        except:
            self._back_up_select(select)
            raise

    # 通用组件：搜索并多选下拉   -- 只适用于DOM文档不剔除其他选取的组件，如优先级
    # TODO -- 待解决：sleep_sec问题
    def key_common_multiselect_with_search(
            self, type_args, search_for: str = None, indexs: str = None, sleep_sec=1):
        """
        :param self:
        :param type_args: by_name传form定位和字段名称，by_loc传如xpath等
        :param search_for: 搜索的字符串内容
        :param indexs: 搜索结果的n个选项
        :param sleep_sec: 搜索后等待
        :return:
        """

        if indexs is None:
            option_idxs = [1, ]
        elif indexs.strip() == 'all':
            option_idxs = [-1, ]
        else:
            option_idxs = indexs.strip().replace('，', ',').split(',')
            option_idxs = [int(i) for i in option_idxs]

        # 多选下拉对象
        loc_type, *args = type_args.replace('，', ',').split(',')
        if loc_type.strip() == 'by_loc':
            multiselect = self._location_field_by_loc(*args)
        elif loc_type.strip() == 'by_name':
            multiselect = self._location_field_by_name(*args)
        else:
            raise Exception('loc_type参数有误')
        # 点击展开下拉框
        multiselect.click()
        self.key_sleep(sleep_sec)   # 添加这个，为了解决api数据源的下拉字段
        try:
            # 选项集ul对象
            options_ul_loc = '//*[@class="tippy-popper"]/div/div[2]/div/div/div[2]/ul'  # 仅ITSM-3.x适用
            options_ul = self.find_element(options_ul_loc)
            all_lis = options_ul.find_elements(By.TAG_NAME, 'li')[1:]

            # 内部搜索框对象
            select_search_loc = '//*[@class="tippy-popper"]/div/div[2]/div/div/div[1]/input'
            self.key_input(select_search_loc, search_for)
            self.key_sleep(sleep_sec)

            visible_lis = [li for li in all_lis if li.value_of_css_property('display') != 'none']

            if not len(visible_lis):
                null_error = "暂无选项"
                raise Exception(null_error)
            li_span_loc = ".//div/label/span"
            if option_idxs[0] == -1:
                for li in visible_lis:
                    li.find_element(By.XPATH, li_span_loc).click()
            else:
                for i in option_idxs:
                    visible_lis[i - 1].find_element(By.XPATH, li_span_loc).click()
        except:
            raise
        finally:
            self._back_up_select(multiselect)

    # 人员组件：搜索并单选下拉   -- 只适用于DOM文档剔除筛选之外的选项，如人员组件
    def key_person_select_with_search(
            self, type_args, search_for: str = None, sleep_sec=2):
        """
        :param self:
        :param type_args: by_name传form定位和字段名称，by_loc传如xpath等
        :param search_for: 搜索的字符串内容
        :param sleep_sec: 搜索后等待
        :return:
        """
        # 单选下拉对象
        loc_type, *args = type_args.replace('，', ',').split(',')
        if loc_type.strip() == 'by_loc':
            select = self._location_field_by_loc(*args)
        elif loc_type.strip() == 'by_name':
            select = self._location_field_by_name(*args)
        else:
            raise Exception('loc_type参数有误')
        # 点击展开下拉框
        select.click()

        try:
            # 内部搜索框对象
            select_search_loc = '//*[@class="tippy-popper"]/div/div[2]/div/div/div[1]/input'
            self.key_input(select_search_loc, search_for)
            self.key_sleep(sleep_sec)

            # 选项集ul对象
            options_ul_loc = '//*[@class="tippy-popper"]/div/div[2]/div/div/div[2]/ul'  # 仅ITSM-3.x适用
            options_ul = self.find_element(options_ul_loc)
            visible_lis = options_ul.find_elements(By.TAG_NAME, 'li')

            if not len(visible_lis):
                null_error = "暂无选项"
                raise Exception(null_error)

            li_1 = visible_lis[0]
            if search_for in li_1.text:
                li_1.click()
            else:
                no_such_option_error = f"选项【{search_for}】搜索不出来"
                raise Exception(no_such_option_error)
        except:
            raise
        finally:
            self._back_up_select(select)

    # 人员组件：搜索并多选下拉   -- 只适用于DOM文档剔除筛选之外的选项，如人员组件
    def key_person_multiselect_with_search(
            self, type_args, search_for: str = None, indexs: str = None, sleep_sec=2):
        """
        :param self:
        :param type_args: by_name传form定位和字段名称，by_loc传如xpath等
        :param search_for: 搜索的字符串内容
        :param indexs: 搜索结果的n个选项, e.g: 1,2,3  ，或者all
        :param sleep_sec: 搜索后等待
        :return:
        """

        if indexs is None:
            option_idxs = [1, ]
        elif indexs.strip() == 'all':
            option_idxs = [-1, ]
        else:
            option_idxs = indexs.strip().replace('，', ',').split(',')
            option_idxs = [int(i) for i in option_idxs]

        # 多选下拉对象
        loc_type, *args = type_args.replace('，', ',').split(',')
        if loc_type.strip() == 'by_loc':
            multiselect = self._location_field_by_loc(*args)
        elif loc_type.strip() == 'by_name':
            multiselect = self._location_field_by_name(*args)
        else:
            raise Exception('loc_type参数有误')
        # 点击展开下拉框
        multiselect.click()

        try:
            # 内部搜索框对象
            select_search_loc = '//*[@class="tippy-popper"]/div/div[2]/div/div/div[1]/input'
            self.key_input(select_search_loc, search_for)
            self.key_sleep(sleep_sec)

            # 选项集ul对象
            options_ul_loc = '//*[@class="tippy-popper"]/div/div[2]/div/div/div[2]/ul'  # 仅ITSM-3.x适用
            options_ul = self.find_element(options_ul_loc)
            visible_lis = options_ul.find_elements(By.TAG_NAME, 'li')

            if not len(visible_lis):
                null_error = "暂无选项"
                raise Exception(null_error)
            li_span_loc = ".//div/div/span"
            if option_idxs[0] == -1:
                for li in visible_lis:
                    li.find_element(By.XPATH, li_span_loc).click()
            else:
                for i in option_idxs:
                    visible_lis[i - 1].find_element(By.XPATH, li_span_loc).click()
        except:
            raise
        finally:
            self._back_up_select(multiselect)

    # 动作：切换浏览器窗口
    def key_switch_window(self, window_index):
        """
        切换浏览器窗口
        :param self:
        :param window_index: 窗口索引
        :return:
        """
        window_handles = self.driver.window_handles
        try:
            current_window = window_handles[int(window_index)]
        except:
            error_msg = f"索引为{window_index}的窗口不存在！"
            raise Exception(error_msg)
        self.driver.switch_to.window(current_window)

    # 动作：切换内嵌弹窗
    def key_switch_ifram(self, ifram_args):
        """
        切换内嵌弹窗
        :param self:
        :param ifram_args: ifram定位参数，如：xpath、class_name、id等
        :return:
        """
        ifram_ele = self.find_element(ifram_args)
        try:
            print(ifram_ele.tag_name)
        except:
            error_msg = f"ifram{ifram_ele}无法定位成功！"
            raise Exception(error_msg)
        self.driver.switch_to.frame(ifram_ele)

    # 动作：文件上传
    def key_upload(self, loc, file):
        ele = self.find_element(loc)
        ele.send_keys(file)

    # 断言：页面文本内容断言
    @allure.step('断言文本内容')
    def key_assert_text(self, loc, text):
        ele_text = self.wait_for_element(By.XPATH or By.ID or By.NAME or By.CLASS_NAME, loc).text

        # 去掉文本内容两边的空格
        ele_text = ele_text.strip()
        text = text.strip()
        logger.info(f'正在断言:实际{ele_text} == 预期{text}')
        assert ele_text == text

    # 断言：获取页面title
    @allure.step('断言页面title')
    def key_page_title(self, expected_title):
        page_title = self.driver.title
        logger.info(f'正在断言:实际:{page_title} = 预期:{expected_title}')
        assert page_title == expected_title
        return True


if __name__ == '__main__':
    ...